Completed
Push — master ( 3c4974...6a66c3 )
by Jeff
03:16
created

Preload.preload   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 0
1
/** global: updateScreenUrl */
2
3
/**
4
 * Screen class constructor
5
 * @param {string} updateScreenUrl global screen update checks url
6
 */
7
function Screen(updateScreenUrl) {
8
  this.fields = [];
9
  this.url = updateScreenUrl;
10
  this.lastChanges = null;
11
  this.endAt = null;
12
  this.nextUrl = null;
13
  this.cache = new Preload(navigator.userAgent.toLowerCase().indexOf('kweb') == -1);
0 ignored issues
show
Bug introduced by
The variable navigator seems to be never declared. If this is a global, consider adding a /** global: navigator */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
14
}
15
16
/**
17
 * Ajax GET on updateScreenUrl to check lastChanges timestamp and reload if necessary
18
 */
19
Screen.prototype.checkUpdates = function() {
20
  var s = this;
21
  $.get(this.url, function(j) {
22
    if (j.success) {
23
      if (s.lastChanges == null) {
24
        s.lastChanges = j.data.lastChanges;
25
      } else if (s.lastChanges != j.data.lastChanges) {
26
        // Remote screen updated, we should reload as soon as possible
27
        s.nextUrl = null;
28
        s.reloadIn(0);
29
        return;
30
      }
31
32
      if (j.data.duration > 0) {
33
        // Setup next screen
34
        s.nextUrl = j.data.nextScreenUrl;
35
        s.reloadIn(j.data.duration * 1000);
36
      }
37
    } else if (j.message == 'Unauthorized') {
38
      // Cookie/session gone bad, try to refresh with full screen reload
39
      screen.reloadIn(0);
40
    }
41
  });
42
}
43
44
/**
45
 * Start Screen reload procedure, checking for every field timeout
46
 */
47
Screen.prototype.reloadIn = function(minDuration) {
48
  var endAt = Date.now() + minDuration;
49
  if (this.endAt != null && this.endAt < endAt) {
50
    // Already going to reload sooner than asked
51
    return;
52
  }
53
54
  if (this.cache.hasPreloadingContent(true)) {
55
    // Do not break preloading
56
    return;
57
  }
58
59
  this.endAt = Date.now() + minDuration;
60
  for (var i in this.fields) {
61
    if (!this.fields.hasOwnProperty(i)) {
62
      continue;
63
    }
64
    var f = this.fields[i];
65
    if (f.timeout && f.endAt > this.endAt) {
66
      // Always wait for content display end
67
      this.endAt = f.endAt;
68
    }
69
  }
70
71
  this.reloadOnTimeout();
72
}
73
74
/**
75
 * Check if we're past the screen.endAt timeout and reload if necessary
76
 * @return {boolean} going to reload
77
 */
78
Screen.prototype.reloadOnTimeout = function() {
79
  if (this.endAt != null && Date.now() >= this.endAt) {
80
    // No content to delay reload, do it now
81
    this.reloadNow();
82
    return true;
83
  }
84
85
  return false;
86
}
87
88
/**
89
 * Actual Screen reload/change screen action
90
 */
91
Screen.prototype.reloadNow = function() {
92
  if (this.nextUrl) {
93
    window.location = this.nextUrl;
94
  } else {
95
    window.location.reload();
96
  }
97
}
98
99
/**
100
 * Check every field for content
101
 * @param  {Content} data
102
 * @return {boolean} content is displayed
103
 */
104
Screen.prototype.displaysData = function(data) {
105
  return this.fields.filter(function(field) {
106
    return field.current && field.current.data == data;
107
  }).length > 0;
108
}
109
110
/**
111
 * Trigger pickNext on all fields
112
 */
113
Screen.prototype.newContentTrigger = function() {
114
  for (var f in this.fields) {
115
    if (!this.fields.hasOwnProperty(f)) {
116
      continue;
117
    }
118
119
    this.fields[f].pickNextIfNecessary();
120
  }
121
}
122
123
/**
124
 * Loop through all fields for stuckiness state
125
 * @return {boolean} are all fields stuck
126
 */
127
Screen.prototype.isAllFieldsStuck = function() {
128
  for (var f in this.fields) {
129
    if (!this.fields.hasOwnProperty(f)) {
130
      continue;
131
    }
132
133
    if (!this.fields[f].stuck && this.fields[f].canUpdate) {
134
      return false;
135
    }
136
  }
137
138
  return true;
139
}
140
141
142
/**
143
 * Content class constructor
144
 * @param {array} c content attributes
145
 */
146
function Content(c) {
147
  this.id = c.id;
148
  this.data = c.data;
149
  this.duration = c.duration * 1000;
150
  this.type = c.type;
151
  this.src = null;
152
153
  if (this.shouldPreload()) {
154
    this.queuePreload();
155
  }
156
}
157
158
/**
159
 * Check if content should be ajax preloaded
160
 * @return {boolean} shoud preload
161
 */
162
Content.prototype.shouldPreload = function() {
163
  return this.canPreload() && !this.isPreloadingOrQueued() && !this.isPreloaded();
164
}
165
166
/**
167
 * Check if content has pre-loadable material
168
 * @return {boolean} can preload
169
 */
170
Content.prototype.canPreload = function() {
171
  return this.getResource() && this.type.search(/Video|Image/) != -1;
172
}
173
174
/**
175
 * Check if content is displayable (preloaded and not too long)
176
 * @return {boolean} can display
177
 */
178
Content.prototype.canDisplay = function() {
179
  return (screen.endAt == null || Date.now() + this.duration < screen.endAt) && this.isPreloaded() && this.data;
180
}
181
182
/**
183
 * Extract url from contant data
184
 * @return {string} resource url
185
 */
186
Content.prototype.getResource = function() {
187
  if (this.src) {
188
    return this.src;
189
  }
190
  var srcMatch = this.data.match(/src="([^"]+)"/);
191
  if (!srcMatch) {
192
    // All preloadable content comes with a src attribute
193
    return false;
194
  }
195
  var src = srcMatch[1];
196
  if (src.indexOf('/') === 0) {
197
    src = window.location.origin + src;
198
  }
199
  if (src.indexOf('http') !== 0) {
200
    return false;
201
  }
202
  // Get rid of fragment
203
  src = src.replace(/#.*/g, '');
204
205
  this.src = src;
206
  return src;
207
}
208
209
/**
210
 * Set content cache status
211
 * @param {string} state preload state
212
 */
213
Content.prototype.setPreloadState = function(state) {
214
  screen.cache.setState(this.getResource(), state);
215
}
216
217
/**
218
 * Check cache for preload status of content
219
 * @return {boolean} is preloaded
220
 */
221
Content.prototype.isPreloaded = function() {
222
  if (!this.canPreload()) {
223
    return true;
224
  }
225
226
  return screen.cache.isPreloaded(this.getResource());
227
}
228
229
/**
230
 * Check cache for in progress or future preloading
231
 * @return {boolean} is preloading
232
 */
233
Content.prototype.isPreloadingOrQueued = function() {
234
  return this.isPreloading() || this.isInPreloadQueue();
235
}
236
237
/**
238
 * Check cache for in progress preloading
239
 * @return {boolean} is preloading
240
 */
241
Content.prototype.isPreloading = function() {
242
  return screen.cache.isPreloading(this.getResource());
243
}
244
245
/**
246
 * Check cache for queued preloading
247
 * @return {boolean} is in preload queue
248
 */
249
Content.prototype.isInPreloadQueue = function() {
250
  return screen.cache.isInPreloadQueue(this.getResource());
251
}
252
253
/**
254
 * Call to preload content
255
 */
256
Content.prototype.preload = function() {
257
  var src = this.getResource();
258
  if (!src) {
259
    return;
260
  }
261
262
  screen.cache.preload(src);
263
}
264
265
/**
266
 * Preload content or add to preload queue
267
 */
268
Content.prototype.queuePreload = function() {
269
  var src = this.getResource();
270
  if (!src) {
271
    return;
272
  }
273
274
  if (screen.cache.hasPreloadingContent(false)) {
275
    this.setPreloadState(Preload.state.PRELOADING_QUEUE);
276
  } else {
277
    this.preload();
278
  }
279
}
280
281
282
/**
283
 * Preload class constructor
284
 * Build cache map
285
 * @param {boolean} isModernBrowser does browser support modern tags
286
 */
287
function Preload(isModernBrowser) {
288
  this.cache = {};
289
  if (isModernBrowser) {
290
    this.preload = this.preloadPrefetch;
291
  } else {
292
    this.preload = this.preloadExternal;
293
  }
294
}
295
296
/**
297
 * Set resource cache state
298
 * @param {string} res   resource url
299
 * @param {int}    state preload state
300
 */
301
Preload.prototype.setState = function(res, state) {
302
  this.cache[res] = state;
303
}
304
305
/**
306
 * Check resource cache for readyness state
307
 * @param  {string}  res resource url
308
 * @return {boolean}     is preloaded
309
 */
310
Preload.prototype.isPreloaded = function(res) {
311
  return this.cache[res] === Preload.state.OK;
312
}
313
314
/**
315
 * Check resource cache for preloading state
316
 * @param  {string}  res resource url
317
 * @return {boolean}     is currently preloading
318
 */
319
Preload.prototype.isPreloading = function(res) {
320
  return this.cache[res] === Preload.state.PRELOADING;
321
}
322
323
/**
324
 * Check resource cache for queued preloading state
325
 * @param  {string}  res resource url
326
 * @return {boolean}     is in preload queue
327
 */
328
Preload.prototype.isInPreloadQueue = function(res) {
329
  return this.cache[res] === Preload.state.PRELOADING_QUEUE;
330
}
331
332
/**
333
 * Check resource cache for queued preloading state during preloader pick phase
334
 * @param  {string}  res resource url
335
 * @return {boolean}     is in preload queue
336
 */
337
Preload.prototype.isWaiting = function(res) {
338
  return this.cache[res] === Preload.state.WAIT_PRELOADER;
339
}
340
341
/**
342
 * Scan resource cache for preloading resources
343
 * @param  {boolean} withQueue also check preload queue
344
 * @return {boolean}           has any resource preloading/in preload queue
345
 */
346
Preload.prototype.hasPreloadingContent = function(withQueue) {
347
  for (var res in this.cache) {
348
    if (!this.cache.hasOwnProperty(res)) {
349
      continue;
350
    }
351
352
    if (this.isPreloading(res) || this.isWaiting(res) || (withQueue && this.isInPreloadQueue(res))) {
353
      return true;
354
    }
355
  }
356
357
  return false;
358
}
359
360
/**
361
 * Preload a resource
362
 * Default implementation waits for preloader pick
363
 * @param {string} res resource url
364
 */
365
Preload.prototype.preload = function(res) {
366
  this.setState(res, Preload.state.WAIT_PRELOADER);
367
}
368
369
/**
370
 * Triggered on preloader picked
371
 * Restarts preload queue processing
372
 */
373
Preload.prototype.preloaderReady = function() {
374
  for (var res in this.cache) {
375
    if (!this.cache.hasOwnProperty(res)) {
376
      continue;
377
    }
378
379
    if (this.isWaiting(res)) {
380
      this.preload(res);
381
    }
382
  }
383
}
384
385
/**
386
 * Preload a resource by calling external preloader
387
 * @param {string} res resource url
388
 */
389
Preload.prototype.preloadExternal = function(res) {
390
  this.setState(res, Preload.state.PRELOADING);
391
  $.ajax("http://127.0.0.1:8089/pf?res=" + res).done(function(j) {
392
    switch (j.state) {
393
      case Preload.state.OK:
0 ignored issues
show
Bug introduced by
The variable Preload seems to be never declared. If this is a global, consider adding a /** global: Preload */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
394
      case Preload.state.NO_CONTENT:
395
        screen.cache.setState(res, j.state);
396
        screen.newContentTrigger();
397
        break;
398
      case Preload.state.HTTP_FAIL:
399
        screen.cache.setState(res, j.state);
400
        break;
401
      case Preload.state.ERR:
402
        screen.cache.setState(res, Preload.state.HTTP_FAIL);
403
        break;
404
      default:
405
        return;
406
    }
407
    screen.cache.preloadNext();
408
  }).fail(function() {
409
    screen.cache.preload = screen.cache.preloadAjax;
410
    screen.cache.preload(res);
411
  });
412
}
413
414
/**
415
 * Preload a resource by addinh a <link rel="prefetch"> tag
416
 * @param {string} res resource url
417
 */
418
Preload.prototype.preloadPrefetch = function(res) {
419
  this.setState(res, Preload.state.PRELOADING);
420
  $('body').append(
421
    $('<link>', {
422
      rel: 'prefetch',
423
      href: res
424
    }).load(function() {
425
      screen.cache.setState(res, Preload.state.OK);
426
      screen.newContentTrigger();
427
      screen.cache.preloadNext();
428
    }).error(function() {
429
      screen.cache.setState(res, Preload.state.HTTP_FAIL);
430
      screen.cache.preloadNext();
431
    })
432
  );
433
}
434
435
/**
436
 * Preload a resource by ajax get on the url
437
 * Check HTTP return state to validate proper cache
438
 * @param {string} res resource url
439
 */
440
Preload.prototype.preloadAjax = function(res) {
441
  this.setState(res, Preload.state.PRELOADING);
442
  $.ajax(res).done(function(data) {
443
    // Preload success
444
    if (data === '') {
445
      screen.cache.setState(res, Preload.state.NO_CONTENT);
446
    } else {
447
      screen.cache.setState(res, Preload.state.OK);
448
    }
449
    screen.newContentTrigger();
450
  }).fail(function() {
451
    // Preload failure
452
    screen.cache.setState(res, Preload.state.HTTP_FAIL);
453
  }).always(function() {
454
    screen.cache.preloadNext();
455
  });
456
}
457
458
/**
459
 * Try to preload next resource or trigger preload end event
460
 */
461
Preload.prototype.preloadNext = function() {
462
  var res = screen.cache.next();
463
  if (res) {
464
    // Preload ended, next resource
465
    screen.cache.preload(res);
466
    return;
467
  }
468
  // We've gone through all queued resources
469
  // Check if we should reload early
470
  if (screen.reloadOnTimeout()) {
471
    return;
472
  }
473
  // Trigger another update to calculate a proper screen.endAt value
474
  screen.checkUpdates();
475
}
476
477
/**
478
 * Get next resource to preload from queue
479
 * @return {string|null} next resource url
480
 */
481
Preload.prototype.next = function() {
482
  for (var res in this.cache) {
483
    if (!this.cache.hasOwnProperty(res)) {
484
      continue;
485
    }
486
487
    if (this.isInPreloadQueue(res)) {
488
      return res;
489
    }
490
  }
491
  return null;
492
}
493
494
/**
495
 * Preload states
496
 */
497
Preload.state = {
498
  ERR: -1,
499
  WAIT_PRELOADER: 1,
500
  PRELOADING: 2,
501
  PRELOADING_QUEUE: 3,
502
  OK: 4,
503
  NO_CONTENT: 5,
504
  HTTP_FAIL: 6,
505
}
506
507
508
/**
509
 * Field class constructor
510
 * @param {jQuery.Object} $f field object
511
 */
512
function Field($f) {
513
  this.$field = $f;
514
  this.id = $f.attr('data-id');
515
  this.url = $f.attr('data-url');
516
  this.types = $f.attr('data-types').split(' ');
517
  this.canUpdate = this.url != null;
518
  this.contents = [];
519
  this.previous = null;
520
  this.current = null;
521
  this.next = null;
522
  this.timeout = null;
523
  this.endAt = null;
524
  this.stuck = false;
525
}
526
527
/**
528
 * Retrieves contents from backend for this field
529
 */
530
Field.prototype.fetchContents = function() {
531
  if (!this.canUpdate) {
532
    return;
533
  }
534
535
  var f = this;
536
  $.get(this.url, function(j) {
537
    if (j.success) {
538
      f.contents = j.next.map(function(c) {
539
        return new Content(c);
540
      });
541
      f.pickNextIfNecessary();
542
    } else {
543
      f.setError(j.message || 'Error');
544
    }
545
  });
546
}
547
548
/**
549
 * Display error in field text
550
 */
551
Field.prototype.setError = function(err) {
552
  this.display(err);
553
}
554
555
/**
556
 * Randomize order
557
 */
558
Field.prototype.randomizeSortContents = function() {
559
  this.contents = this.contents.sort(function() {
560
    return Math.random() - 0.5;
561
  });
562
}
563
564
/**
565
 * PickNext if no content currently displayed and content is available
566
 */
567
Field.prototype.pickNextIfNecessary = function() {
568
  if (!this.timeout) {
569
    this.pickNext();
570
  }
571
}
572
573
/**
574
 * Loop through field contents to pick next displayable content
575
 */
576
Field.prototype.pickNext = function() {
577
  if (screen.reloadOnTimeout()) {
578
    // Currently trying to reload, we're past threshold: reload now
579
    return;
580
  }
581
582
  // Keep track of true previous content
583
  if (this.current != null) {
584
    this.previous = this.current;
585
  }
586
  this.current = null;
587
  var previousData = this.previous && this.previous.data;
588
589
  this.next = this.pickRandomContent(previousData) || this.pickRandomContent(previousData, true);
590
591
  if (this.next) {
592
    // Overwrite field with newly picked content
593
    this.displayNext();
594
    this.stuck = false;
595
  } else {
596
    // I am stuck, don't know what to display
597
    this.stuck = true;
598
    // Check other fields for stuckiness state
599
    if (screen.isAllFieldsStuck() && !screen.cache.hasPreloadingContent(true)) {
600
      // Nothing to do. Give up, reload now
601
      screen.reloadNow();
602
    }
603
  }
604
}
605
606
/**
607
 * Loop through field contents for any displayable content
608
 * @param  {string}  previousData previous content data
609
 * @param  {boolean} anyUsable    ignore constraints
610
 * @return {Content}              random usable content
611
 */
612
Field.prototype.pickRandomContent = function(previousData, anyUsable) {
613
  this.randomizeSortContents();
614
  for (var i = 0; i < this.contents.length; i++) {
615
    var c = this.contents[i];
616
    // Skip too long, not preloaded or empty content
617
    if (!c.canDisplay()) {
618
      continue;
619
    }
620
621
    if (anyUsable) {
622
      // Ignore repeat & same content constraints if necessary
623
      return c;
624
    }
625
626
    // Avoid repeat same content
627
    if (c.data == previousData) {
628
      // Not enough content, display anyway
629
      if (this.contents.length < 2) {
630
        return c;
631
      }
632
      continue;
633
    }
634
635
    // Avoid same content than already displayed on other fields
636
    if (screen.displaysData(c.data)) {
637
      // Not enough content, display anyway
638
      if (this.contents.length < 3) {
639
        return c;
640
      }
641
      continue;
642
    }
643
644
    // Nice content. Display it.
645
    return c;
646
  }
647
  return null;
648
}
649
650
/**
651
 * Setup next content for field and display it
652
 */
653
Field.prototype.displayNext = function() {
654
  var f = this;
655
  if (this.next && this.next.duration > 0) {
656
    this.current = this.next
657
    this.next = null;
658
    this.display(this.current.data);
659
    if (this.timeout) {
660
      clearTimeout(this.timeout);
661
    }
662
    this.endAt = Date.now() + this.current.duration;
663
    this.timeout = setTimeout(function() {
664
      f.pickNext();
665
    }, this.current.duration);
666
  }
667
}
668
669
/**
670
 * Display data in field HTML
671
 * @param {string} data
672
 */
673
Field.prototype.display = function(data) {
674
  this.$field.html(data);
675
  this.$field.show();
676
  var $bt = this.$field.find('.bigtext');
677
  // Only first data-min/max per field is respected
678
  var minPx = $bt.attr('data-min-px') || 4;
679
  var maxPx = $bt.attr('data-max-px') || 0;
680
  $bt.parent().textfill({
681
    minFontPixels: minPx,
682
    maxFontPixels: maxPx,
683
  });
684
}
685
686
// Global screen instance
687
var screen = null;
688
689
/**
690
 * jQuery.load event
691
 * Initialize Screen and Fields
692
 * Setup updates interval timeouts
693
 */
694
function onLoad() {
695
  screen = new Screen(updateScreenUrl);
696
  // Init
697
  $('.field').each(function() {
698
    var f = new Field($(this));
699
    screen.fields.push(f);
700
    f.fetchContents();
701
  });
702
703
  if (screen.url) {
704
    // Setup content updates loop
705
    setInterval(function() {
706
      for (var f in screen.fields) {
707
        if (screen.fields.hasOwnProperty(f)) {
708
          screen.fields[f].fetchContents();
709
        }
710
      }
711
      screen.checkUpdates();
712
    }, 60000); // 1 minute is enough alongside preload queue end trigger
713
    screen.checkUpdates();
714
  }
715
}
716
717
// Run
718
$(onLoad);
719